/*\
|*| chatabase JSON request test runner
|*|
|*| this script will mutate the host prim into a 5-face font prim to be worn HUD bottom
|*|     each of the 5-faces act as triggers assignable to run any single suite or all
|*| the object inventory will be searched for scripts named like: "00 SomeModel tests"
|*|     the 2nd to the last space-delimited token will be the suite name (e.g. "SomeModel")
|*|     the leading numbers are only for inventory sorted ordering
|*|     but suite scripts will loaded and launched in that order
|*| this main script is quite general and will require little modification
|*|     all of the logic is carried out in this script
|*| all endpoints, test fixtures, and expected results are data in the suite scripts
|*|     data is loaded into this main script as needed per test via link-message
|*|     project-specific shared data is sent to each suite script before loading data
\*/


// runtime switches
integer DEBUG           = FALSE ;
integer DEBUG_EVS       = FALSE ;
integer DEBUG_VB        = FALSE ;
integer HALT_ON_FAILURE = TRUE ;

// shared project-specific data
string  GRID_NAME          = "SecondLife" ;
string  ROOM_NAME_SENTINAL = "{{ROOM_NAME}}" ;
string  EXCEPTION_RESP     = "!!!EXCEPTION!!!" ;
string  SECRET_RESP        = "{\"secret\":\"" ;
string  OWNER_NAME ;  // init()
string  REGION_NAME ; // init()
string  PARCEL_NAME ; // init()
integer GRID_IDX      = -1 ;
integer REGION_IDX    = -2 ;
integer PARCEL_IDX    = -3 ;
integer SENTINEL_IDX  = -4 ;
integer EXCEPTION_IDX = -5 ;
integer SECRET_IDX    = -6 ;
integer OWNER_IDX     = -7 ;

// project-specific data
string  SERVER_URL        = "" ;
integer SHOULD_VERIFY_SSL = TRUE ;
string  INDEX_PATH        = "/" ;

// link messages
integer DATA_LOAD_IDX   = -420 ;
key     DATA_RETURN_EVT = "data" ;
integer DESCRIPTION_IDX = 0 ;
integer REQUEST_IDX     = 1 ;
integer RESPONSE_IDX    = 2 ;
integer STATUS_IDX      = 3 ;
integer ENDPOINT_IDX    = 4 ;
list    TEST_EVTS ; // init()
integer N_SUITES ;  // init()

// http params
float POST_THROTTLE_IVL = 1.0 ;
list  JSON_POST_PARAMS  = [ HTTP_METHOD , "POST" , HTTP_MIMETYPE , "application/json" ,
                            HTTP_VERIFY_CERT , FALSE                                  ] ;

// http status
integer HTTP_STATUS_SUCCESS = 200 ;
integer HTTP_STATUS_FAIL    = 420 ;

// prims params
integer FACE_L_OUTER = 3 ;
integer FACE_L_INNER = 7 ;
integer FACE_C       = 4 ;
integer FACE_R_INNER = 6 ;
integer FACE_R_OUTER = 1 ;
string  FLOAT_TEXT   = "'register'     'post'        'all'        'read'       'unused'" ;
vector  TEXT_COLOR   = <1.0 , 1.0 , 1.0> ;
list    PRIM_PARAMS  = [ PRIM_TYPE , PRIM_TYPE_PRISM , PRIM_HOLE_SQUARE ,
                                     <0.199 , 0.8 , 0.0> , 0.30 , ZERO_VECTOR ,
                                     <1.0 , 1.0 , 0.0> , ZERO_VECTOR ,
                         PRIM_SIZE , <0.03 , 0.5 , 0.125>                                     ] ;

// lsl output
string VERIFYING_MSG      = " test suites found - Verifying server at: " ;
string SERVER_EXISTS_MSG  = "Server returned the expected response" ;
string SERVER_NO_RESP_MSG = "Server did not return the expected response" ;
string INVALID_DATA_MSG   = "ERROR: test data invalid or empty" ;
string TEST_COMPLETE_MSG  = "Test[" ;
string TEST_FAILURE_MSG   = "] failed:" ;
string TEST_SUCCESS_MSG   = "] passed:" ;


// runtime vars
integer ShouldRunAllSuites = TRUE ;
integer SuiteN ;               // setSuite()
key     SuiteName ;            // setSuite()
integer TestN ;                // setSuite()
integer NPassed ;              // setSuite()
integer NTests ;               // validateSuite()
string  TestsEndpoint ;        // active::link_message()
string  TestRequestJson ;      // active::link_message()
string  ExpectedResponseJson ; // active::link_message()
integer ExpecteedStatus ;      // active::link_message()
string  TestDescription ;      // active::link_message()


/* state default functions */

init()
{
  // set shared data
  llSetObjectDesc("") ;
  list parcel_details = llGetParcelDetails(llGetPos() , [PARCEL_DETAILS_NAME]) ;
  OWNER_NAME          = llKey2Name(llGetOwner()) ;
  REGION_NAME         = strReplace(llGetRegionName()                 , " " , "_") ;
  PARCEL_NAME         = strReplace(llList2String(parcel_details , 0) , " " , "_") ;

  // configure controller prim
  llSetPrimitiveParams(PRIM_PARAMS) ;
  llSetText(FLOAT_TEXT , TEXT_COLOR , 1.0) ;

  // detect test suite scripts and cache suite names
  integer script_n ; integer n_scripts = llGetInventoryNumber(INVENTORY_SCRIPT) ;
  for (script_n = 0 ; script_n < n_scripts ; ++script_n)
  {
    string script_name = llGetInventoryName(INVENTORY_SCRIPT , script_n) ;
    list   tokens      = llParseString2List(script_name , [" "] , []) ;

    if (llList2String(tokens , -1) == "tests") TEST_EVTS += llList2Key(tokens , -2) ;
  }
  N_SUITES = llGetListLength(TEST_EVTS) ;

  // set initial state
  setSuite(0) ;

  // verify server
  string url = llToLower(SERVER_URL) + llToLower(INDEX_PATH) ;
  TRACE((string)N_SUITES + VERIFYING_MSG + url) ;
  llHTTPRequest(url , [] , "") ;

DBG("CONTROLLER::REQ -> " + url) ;
}

integer handleTouch(integer face_n)
{
if (DEBUG_EVS) { integer n = face_n ; string f ; if (n == FACE_L_OUTER) f = "FACE_L_OUTER" ; else if (n == FACE_L_INNER) f = "FACE_L_INNER" ; else if (n == FACE_C) f = "FACE_C" ; else if (n == FACE_R_INNER) f = "FACE_R_INNER unhandled" ; else if (n == FACE_R_OUTER) f = "FACE_R_OUTER" ; else f = "unknown" ; DBG_EVS("handleTouch() face=" + f) ; }

  if      (face_n == FACE_L_OUTER) { setSuite(0) ; ShouldRunAllSuites = FALSE ; }
  else if (face_n == FACE_L_INNER) { setSuite(1) ; ShouldRunAllSuites = FALSE ; }
  else if (face_n == FACE_C      ) { setSuite(2) ; ShouldRunAllSuites = FALSE ; }
  else if (face_n == FACE_R_INNER) { return FALSE ;                             }
  else if (face_n == FACE_R_OUTER) { setSuite(0) ; ShouldRunAllSuites = TRUE ;  }
  else                             { return FALSE ;                             }

  return TRUE ;
}

setSuite(integer suite_n)
{
  SuiteN    = suite_n ;
  NTests    = 0 ;
  TestN     = 0 ;
  NPassed   = 0 ;
  SuiteName = llList2Key(TEST_EVTS , suite_n) ;
  resetTestData() ;
}

resetTestData()
{
  TestsEndpoint        = "" ;
  TestRequestJson      = "" ;
  ExpectedResponseJson = "" ;
  ExpecteedStatus      = -1 ;
  TestDescription      = "" ;
}


/* state launch_suite functions */

sendSharedDataLinkMsgs()
{
  sendLinkMsg(DATA_LOAD_IDX , ""                ) ;
  sendLinkMsg(GRID_IDX      , GRID_NAME         ) ;
  sendLinkMsg(REGION_IDX    , REGION_NAME       ) ;
  sendLinkMsg(PARCEL_IDX    , PARCEL_NAME       ) ;
  sendLinkMsg(SENTINEL_IDX  , ROOM_NAME_SENTINAL) ;
  sendLinkMsg(EXCEPTION_IDX , EXCEPTION_RESP    ) ;
  sendLinkMsg(SECRET_IDX    , SECRET_RESP       ) ;
  sendLinkMsg(OWNER_IDX     , OWNER_NAME        ) ;
}

integer nextSuite()
{
  integer n_suites_remaining = N_SUITES - ++SuiteN ;
  integer should_halt        = NPassed / NTests == 0 && HALT_ON_FAILURE ;
  ShouldRunAllSuites         = n_suites_remaining > 0 && !should_halt ;

DBG("nextSuite() n_suites_remaining=" + (string)n_suites_remaining + " should_halt=" + (string)should_halt + " ShouldRunAllSuites=" + (string)ShouldRunAllSuites) ;

  if (ShouldRunAllSuites) setSuite(SuiteN) ;

  return ShouldRunAllSuites ;
}

integer validateSuite(string n_tests)
{
  integer is_valid_test_suite = (NTests = (integer)n_tests) > 0 ;

if (DEBUG_VB) DBG("validateSuite(" + SuiteName + ") n_tests=" + n_tests + " is_valid_test_suite=" + (string)is_valid_test_suite) ;

  string init_msg = " (" + (string)NTests + ") '" + SuiteName + "' tests" ;
  if (is_valid_test_suite) TRACE("Running"                     + init_msg) ;
  else                     TRACE(INVALID_DATA_MSG + " running" + init_msg) ;

  return is_valid_test_suite ;
}


/* state run_test functions */

integer loadTestData(integer data_idx , string data)
{
  string  data_as_string  = strReplace(data , "'" , "\"") ;
  integer data_as_integer = (integer)data ;

  if      (data_idx == ENDPOINT_IDX   ) TestsEndpoint        = data_as_string ;
  else if (data_idx == REQUEST_IDX    ) TestRequestJson      = data_as_string ;
  else if (data_idx == RESPONSE_IDX   ) ExpectedResponseJson = data_as_string ;
  else if (data_idx == STATUS_IDX     ) ExpecteedStatus      = data_as_integer ;
  else if (data_idx == DESCRIPTION_IDX) TestDescription      = data_as_string ;

  return TestsEndpoint   != "" && TestRequestJson != "" && ExpectedResponseJson != "" &&
         ExpecteedStatus != -1 && TestDescription != ""                                ;
}

sendRequest()
{
  list   post_params = llListReplaceList(JSON_POST_PARAMS , SHOULD_VERIFY_SSL , -1 , -1) ;
  string url         = llToLower(SERVER_URL) + TestsEndpoint ;

  llSleep(POST_THROTTLE_IVL) ;
  llHTTPRequest(url , post_params , TestRequestJson) ;

DBG_EVS("CONTROLLER::REQ -> (" + (string)TestN + ") '" + TestDescription + "' " + TestRequestJson) ;
}

integer handleResponse(string actual_response , integer status)
{
  integer was_success   = status == ExpecteedStatus ;
  integer did_test_pass = (was_success && ExpectedResponseJson == EXCEPTION_RESP) ||
                          validateResponse(actual_response , ExpectedResponseJson) ;
  string  result        = TEST_COMPLETE_MSG + (string)TestN ;
  if (did_test_pass) result += TEST_SUCCESS_MSG + " '" + TestDescription + "'" ;
  else               result += TEST_FAILURE_MSG + " '" + TestDescription + "'" ;
  if (!did_test_pass)
    if (was_success) result += "\nexpected:\n"         + ExpectedResponseJson +
                               "\nactual:\n"           + actual_response      ;
    else             result += "\nexpected status:\n"  + ExpecteedStatus +
                               "\nactual status:\n"    + (string)status  ;

  TRACE(result) ;

  // store project-specific server-generated session data
  //string secret = llJsonGetValue(actual_response , [ "secret" ]) ;
  if (ExpectedResponseJson == SECRET_RESP && did_test_pass)
  {
    string secret = llGetSubString(actual_response , llStringLength(SECRET_RESP) , -3) ;

    if (secret != "") llSetObjectDesc(secret) ;
  }

if (DEBUG_EVS) { string dbg = "CONTROLLER::RESP<-" ; if (ExpectedResponseJson == SECRET_RESP && did_test_pass) { string secret = llGetObjectDesc() ; if (secret != "") dbg += " got secret='" + secret + "'" ; else dbg = " ERROR: empty secret" ; } if (DEBUG_VB && did_test_pass) dbg += " resp=\n" + actual_response ; if (dbg != "CONTROLLER::RESP<-") DBG(dbg) ; }

  return did_test_pass ;
}


/* helpers */

TRACE  (string msg) {                llSay     (PUBLIC_CHANNEL , msg) ; }
DBG    (string msg) { if (DEBUG    ) llOwnerSay(msg                 ) ; }
DBG_EVS(string msg) { if (DEBUG_EVS) llOwnerSay(msg                 ) ; }
DBG_VB (string msg) { if (DEBUG_VB ) DBG       (msg                 ) ; }

sendLinkMsg(integer data_idx , string data)
{
DBG("CONTROLLER::SEND-> " + llList2CSV([SuiteName , data_idx , data])) ;

  llMessageLinked(LINK_THIS , data_idx , data , SuiteName) ;
}

string strReplace(string str , string sentinel , string replace)
{
  return llDumpList2String(llParseStringKeepNulls(str , [sentinel] , []) , replace) ;
}

string stripWhitespace(string src)
{
  return llDumpList2String(llParseString2List(src , [" " , "\n"] , []) , "") ;
}

integer validateResponse(string actual_resp , string expected_resp)
{
  return ~llSubStringIndex(stripWhitespace(actual_resp) , stripWhitespace(expected_resp)) ;
}


default
{
  state_entry() { init() ; }

  http_response(key unused , integer status , list notused , string body)
  {
if (DEBUG_EVS) { string dbg = "CONTROLLER::RESP<- " + (string)status ; if (DEBUG_VB) DBG_VB(dbg + " body=\n" + body) ; else if (status != HTTP_STATUS_SUCCESS) DBG(dbg) ; }

    if (status == HTTP_STATUS_SUCCESS) { TRACE(SERVER_EXISTS_MSG ) ; state ready ; }
    else                               { TRACE(SERVER_NO_RESP_MSG) ; state dead ;  }
  }
}


state ready
{
  state_entry() { if (ShouldRunAllSuites) state launch_suite ; }

  touch_start(integer n_events)
  {
    if (handleTouch(llDetectedTouchFace(0))) state launch_suite ;
  }
}


state next_suite { state_entry() { if (nextSuite()) state launch_suite ; else state ready ; } }
state launch_suite
{
  state_entry() { sendSharedDataLinkMsgs() ; }

  link_message(integer sender , integer data_idx , string data , key evt)
  {
    if (evt != DATA_RETURN_EVT) return ;

DBG_EVS("CONTROLLER::RECV<- " + llList2CSV([evt , data_idx , data])) ;

    if (data_idx != DATA_LOAD_IDX) return ;

    if (validateSuite(data)) state next_test ;
    else                     state next_suite ;
  }

  touch_start(integer n) { state ready ; }
}


state next_test { state_entry() { state run_test ; } } state run_test
{
  state_entry() { resetTestData() ; sendLinkMsg(TestN , "") ; }

  link_message(integer sender , integer data_idx , string data , key evt)
  {
    if (evt != DATA_RETURN_EVT) return ;

DBG_EVS("CONTROLLER::RECV<- " + llList2CSV([evt , data_idx , data])) ;

    if (loadTestData(data_idx , data)) sendRequest() ;
  }

  http_response(key resp_id , integer status , list meta , string body)
  {
    integer did_test_pass        = !!handleResponse(body , status) ;
    integer n_tests_remaining    = NTests - ++TestN ;
    integer should_halt          = !did_test_pass && HALT_ON_FAILURE ;
    integer should_run_next_test = n_tests_remaining > 0 && !should_halt ;
    NPassed                     += did_test_pass ;

DBG("run_test::http_response() did_test_pass=" + (string)did_test_pass + " should_halt=" + (string)should_halt + " n_tests_remaining=" + (string)n_tests_remaining + " should_run_next_test=" + (string)should_run_next_test) ;

    if (should_run_next_test) state next_test ;

    // suite done
    string results_msg = (string)NPassed + "/" + (string)NTests + " '" + SuiteName + "' tests passed" ;
    if (n_tests_remaining > 0) results_msg += " (" + (string)n_tests_remaining + " skipped)" ;
    TRACE(results_msg) ;

    state next_suite ;
  }

  touch_start(integer n) { state ready ; }
}
